package board;

import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.collision.CollisionResult;
import com.jme3.collision.CollisionResults;
import com.jme3.math.FastMath;
import com.jme3.math.Ray;
import com.jme3.math.Vector3f;
import game.DataStore;
import game.Main;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;
import misc.Vector3i;

public abstract class BoardUtil {

    public static Vector3f boardToWorld(Vector3i boardLocation) {
	return getTileWorldLocation(DataStore.board.getTile(boardLocation));
    }
    
    public static Vector3f getTileWorldLocation(Tile tile) {
        return tile.getControl(RigidBodyControl.class).getPhysicsLocation();
    }

    public static int tilesBetweenManhattan(Vector3i location1, Vector3i location2) {
	return Math.round(FastMath.abs(location2.getX() - location1.getX()) +
			  FastMath.abs(location2.getY() - location1.getY()) +
			  FastMath.abs(location2.getZ() - location1.getZ()));
    }

    public static int tilesBetweenEuclidean(Vector3i location1, Vector3i location2) {
	if(location1.getX() == location2.getX())
	    return Math.round(FastMath.abs(location2.getZ() - location1.getZ()));
	else if(location1.getZ() == location2.getZ())
	    return Math.round(FastMath.abs(location2.getX() - location1.getX()));
	
	if(location1.getZ() < location2.getZ()) {
	    if(location1.getX() < location2.getX())
		return 1 + tilesBetweenEuclidean(location1.clone().addX(1).addZ(1), location2);
	    else
		return 1 + tilesBetweenEuclidean(location1.clone().addX(-1).addZ(1), location2);
	} else {
	    if(location1.getX() < location2.getX())
		return 1 + tilesBetweenEuclidean(location1.clone().addX(1).addZ(-1), location2);
	    else
		return 1 + tilesBetweenEuclidean(location1.clone().addX(-1).addZ(-1), location2);
	}
    }

    public static Vector3f getAverageWorldLocation(Collection<Vector3i> boardLocations) {
	float x=0,y=0,z=0;
	for(Vector3i boardLocation : boardLocations) {
	    Vector3f worldLocation = BoardUtil.boardToWorld(boardLocation);
	    x += worldLocation.getX();
	    y += worldLocation.getY();
	    z += worldLocation.getZ();
	}
	int size = boardLocations.size();
	return new Vector3f(x/size, y/size, z/size);
    }
    
    /**
     * Returns all board locations within the given PatternType and Size.
     * Multiple targets may be given if the PatternType requires them (i.e. the Line PatternType).
     */
    public static Collection<Vector3i> getPatternLocations(PatternType patternType, int size, boolean includeFirstTarget, Vector3i... target) {
        Set<Vector3i> patternLocations = new HashSet<Vector3i>();
        if(includeFirstTarget)
            patternLocations.add(target[0].clone());
        
	switch(patternType) {
	    case DIAMOND:
                //square bounds all other shapes of the same size, so it can be used as a baseline
		for(Vector3i location : BoardUtil.getPatternLocations(PatternType.SQUARE, size, includeFirstTarget, target)) {
		    int tilesBetween = tilesBetweenManhattan(target[0], location);
		    if(tilesBetween <= size && tilesBetween != 0)
			patternLocations.add(location.clone());
		}
		break;
		
	    case SQUARE:
		for(int i=-size; i<=size; i++) {
		    for(int j=-size; j<=size; j++) {
			if(i != 0 || j != 0)
			    patternLocations.add(target[0].clone().addX(i).addZ(j));
		    }
		}
		break;
		
	    case CIRCLE:
                //right now, circle is just a diamond with the tips cut off
		patternLocations.addAll(BoardUtil.getPatternLocations(PatternType.DIAMOND, size+1, includeFirstTarget, target));
                patternLocations.remove(target[0].clone().addX(size+1));
                patternLocations.remove(target[0].clone().addX(-(size+1)));
                patternLocations.remove(target[0].clone().addZ(size+1));
                patternLocations.remove(target[0].clone().addZ(-(size+1)));
		break;

	    case CROSS:
		for(int i=1; i<=size; i++)
		    patternLocations.add(target[0].clone().addX(i));
		for(int i=1; i<=size; i++)
		    patternLocations.add(target[0].clone().addX(-i));
		for(int i=1; i<=size; i++)
		    patternLocations.add(target[0].clone().addZ(i));
		for(int i=1; i<=size; i++)
		    patternLocations.add(target[0].clone().addZ(-i));
		break;
		
	    case LINE:
                if(target[1].getX()-target[0].getX() == 0) {
                    if(target[1].getZ() > target[0].getZ()) {
                        for(int i=target[0].getZ()+1; i<=target[1].getZ(); i++)
                            patternLocations.add(target[0].clone().setZ(i));
                    } else if(target[1].getZ() < target[0].getZ()) {
                        for(int i=target[0].getZ()-1; i>=target[1].getZ(); i--)
                            patternLocations.add(target[0].clone().setZ(i));
                    }
                } else if(target[1].getZ()-target[0].getZ() == 0) {
                    if(target[1].getX() > target[0].getX()) {
                        for(int i=target[0].getX()+1; i<=target[1].getX(); i++)
                            patternLocations.add(target[0].clone().setX(i));
                    } else if(target[1].getX() < target[0].getX()) {
                        for(int i=target[0].getX()-1; i>=target[1].getX(); i--)
                            patternLocations.add(target[0].clone().setX(i));
                    }
                }
		break;
            
	    case CONE:
		//TODO do this programatically using size
		break;
	}
	Iterator<Vector3i> iter = patternLocations.iterator();
        while (iter.hasNext()) {
            if(!DataStore.board.isTileAtLocation(iter.next())) {
                iter.remove();
            }
        }
        return patternLocations;
    }

    public static Vector3i castDownToBoard(Vector3f location) {
        CollisionResults results = new CollisionResults();
	Ray ray = new Ray(location, new Vector3f(0, -1, 0));
        DataStore.board.collideWith(ray, results);
        for(CollisionResult result : results) {
            if(result.getGeometry().getParent() instanceof Tile) {
                return ((Tile)result.getGeometry().getParent()).getBoardLocation();
            }
        }
        return null;
    }
    
    public static List<Vector3i> findPath(Vector3i start, Vector3i goal) {
        return findPath(start, goal, 2, 3);
    }
    
    public static List<Vector3i> findPath(Vector3i start, Vector3i goal, int cardinalCost, int diagonalCost) {
        Map<Vector3i, Integer> costs = new HashMap<Vector3i, Integer>();  // Cost from start along best known path.
        costs.put(start, 0);
        
        Set<Vector3i> closedset = new HashSet<Vector3i>();  // The set of nodes already evaluated.
        PriorityQueue<BoardLocationWithCost> openset = new PriorityQueue<BoardLocationWithCost>(11, new BoardLocationComparator(goal, cardinalCost, diagonalCost));  // The set of tentative nodes to be evaluated,
        openset.add(new BoardLocationWithCost(start, costs.get(start)));  // initially containing the start node
        
        Map<Vector3i, Vector3i> predecessorMap = new HashMap<Vector3i, Vector3i>();  // The map of navigated nodes.
        
        BoardLocationWithCost current;
        while (!openset.isEmpty()) {
            current = openset.poll();
            
            System.out.println("pathfinding considering: "+current.getBoardLocation()+" which has total cost to goal: "+(current.getCost()+costBetween(current.getBoardLocation(), goal, cardinalCost, diagonalCost)));
            
            if (current.getBoardLocation().equals(goal)) {
                return reconstructPath(predecessorMap, goal);
            }

            openset.remove(current);
            closedset.add(current.getBoardLocation());
            for (Vector3i neighborLocation : neighborLocations(current.getBoardLocation())) {
                if (!closedset.contains(neighborLocation)) {
                    BoardLocationWithCost neighbor = new BoardLocationWithCost(neighborLocation, current.getCost() + costBetween(current.getBoardLocation(), neighborLocation, cardinalCost, diagonalCost));
                    
                    if (!openset.contains(neighbor) || neighbor.getCost() < costs.get(neighborLocation)) {
                        openset.add(neighbor);
                        predecessorMap.put(neighbor.getBoardLocation(), current.getBoardLocation());
                        costs.put(neighbor.getBoardLocation(), neighbor.getCost());
                    }
                }
            }
        }
        
        return null;
    }
    
    /** Same as BoardUtil.tilesBetweenEuclidean, but with cost weights. */
    public static int costBetween(Vector3i location1, Vector3i location2, int cardinalCost, int diagonalCost) {
	if(location1.getX() == location2.getX())
	    return cardinalCost * Math.round(FastMath.abs(location2.getZ() - location1.getZ()));
	else if(location1.getZ() == location2.getZ())
	    return cardinalCost * Math.round(FastMath.abs(location2.getX() - location1.getX()));

	if(location1.getZ() < location2.getZ()) {
	    if(location1.getX() < location2.getX())
		return diagonalCost + costBetween(location1.clone().addX(1).addZ(1), location2, cardinalCost, diagonalCost);
	    else
		return diagonalCost + costBetween(location1.clone().addX(-1).addZ(1), location2, cardinalCost, diagonalCost);
	} else {
	    if(location1.getX() < location2.getX())
		return diagonalCost + costBetween(location1.clone().addX(1).addZ(-1), location2, cardinalCost, diagonalCost);
	    else
		return diagonalCost + costBetween(location1.clone().addX(-1).addZ(-1), location2, cardinalCost, diagonalCost);
	}
    }
    
    protected static Collection<Vector3i> neighborLocations(Vector3i boardLocation) {
        Set<Vector3i> neighbors = new HashSet<Vector3i>();
        neighbors.addAll(BoardUtil.getPatternLocations(PatternType.SQUARE, 1, false, boardLocation));
        Iterator<Vector3i> iter = neighbors.iterator();
        while (iter.hasNext()) {
            if(!DataStore.board.isTileAtLocation(iter.next())) {
                iter.remove();
            }
        }
        return neighbors;
    }
 
    protected static List<Vector3i> reconstructPath(Map<Vector3i, Vector3i> predecessorMap, Vector3i currentNode) {
        if (predecessorMap.containsKey(currentNode)) {
            List<Vector3i> path = reconstructPath(predecessorMap, predecessorMap.get(currentNode));
            path.add(currentNode);
            return path;
        } else {
            return new ArrayList<Vector3i>();
        }
    }
}
